package cn.edu.sjtu.stap.hg;

import java.util.*;

import org.aspectj.ajdt.internal.compiler.ast.*;
import org.aspectj.org.eclipse.jdt.internal.compiler.*;
import org.aspectj.org.eclipse.jdt.internal.compiler.ast.*;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.*;

import cn.edu.sjtu.stap.graph.*;

public class HierarchyGraph extends Graph {
    private static final long serialVersionUID = 1L;

    //private static HierarchyGraph instance = null;

    // This is for performance.
    private HashMap<TypeBinding, TypeNode> typeNodeMapping = new HashMap<TypeBinding, TypeNode>();

    public HierarchyGraph() {
        super();
    }

    /*public static HierarchyGraph getInstance() {
        if (instance == null) {
            instance = new HierarchyGraph();
        }
        return instance;
    }*/

    public void addType(CompilationUnitDeclaration u) {
        ASTVisitor v = new ASTVisitor() {
            public boolean visit(TypeDeclaration localTypeDeclaration, BlockScope scope) {
                addType(localTypeDeclaration);

                return true;
            }

            public boolean visit(TypeDeclaration memberTypeDeclaration, ClassScope scope) {
                addType(memberTypeDeclaration);

                return true;
            }

            public boolean visit(TypeDeclaration typeDeclaration, CompilationUnitScope scope) {
                addType(typeDeclaration);

                return true;
            }
            
            public boolean visit(MethodDeclaration methodDeclaration, ClassScope scope) {
                if (methodDeclaration instanceof InterTypeMethodDeclaration) {
                    TypeBinding type = ((InterTypeMethodDeclaration) methodDeclaration).getOnType().resolvedType;
                    addIntertypeMethod(type, methodDeclaration);
                }
                
                return true;
            }
        };
        u.traverse(v, u.scope);
    }

    private void addType(TypeDeclaration type) {
        TypeNodeKind kind = null;

        if (type.binding.isClass()) {
            kind = TypeNodeKind.CLASS;
        } else if (type.binding.isInterface()) {
            kind = TypeNodeKind.INTERFACE;
        } else if (type instanceof AspectDeclaration) {
            kind = TypeNodeKind.ASPECT;
        }

        TypeNode node = new TypeNode(kind);
        node.setTypeDeclaration(type);
        this.add(node);
        typeNodeMapping.put(type.binding, node);

        resolveTypes(type);
        resolveMethods(type);
    }

    private Set<TypeDeclaration> unresolvedTypes = new HashSet<TypeDeclaration>();

    /**
     * Resolve the inheritance of the given type.
     * 
     * @param type
     */
    private void resolveTypes(TypeDeclaration type) {
        unresolvedTypes.add(type);

        Iterator<TypeDeclaration> i = unresolvedTypes.iterator();
        while (i.hasNext()) {
            boolean shouldRemove = true;
            TypeDeclaration t = i.next();
            TypeNode thisNode = typeNodeMapping.get(t.binding);
            TypeBinding supertype = t.binding.superclass();
            TypeNode superTypeNode = typeNodeMapping.get(supertype);
            if (superTypeNode != null) {
                if (!this.contains(thisNode, superTypeNode)) {
                    addRelationship(thisNode, superTypeNode, RelationshipEdgeKind.EXTENDS);
                }
            } else if (!(supertype instanceof BinaryTypeBinding)) {
                // Is source class.
                shouldRemove = false;
            }

            TypeBinding[] superInterfaces = t.binding.superInterfaces();
            for (int j = 0; j < superInterfaces.length; ++j) {
                TypeNode superInterfaceNode = typeNodeMapping.get(superInterfaces[j]);
                if (superInterfaceNode != null) {
                    if (!this.contains(thisNode, superInterfaceNode)) {
                        addRelationship(thisNode, superInterfaceNode, RelationshipEdgeKind.IMPLEMENTS);
                    }
                } else if (!(superInterfaces[j] instanceof BinaryTypeBinding)) {
                    // Is source interface.
                    shouldRemove = false;
                }
            }

            if (shouldRemove) {
                i.remove();
            }
        }
    }

    private Map<TypeBinding, List<AbstractMethodDeclaration>> unresolvedMethods = new HashMap<TypeBinding, List<AbstractMethodDeclaration>>();

    public void addIntertypeMethod(TypeBinding binding, AbstractMethodDeclaration method) {
        TypeNode node = getTypeNode(binding);

        if (node == null) {
            List<AbstractMethodDeclaration> list = unresolvedMethods.get(binding);
            if (list == null) {
                list = new ArrayList<AbstractMethodDeclaration>();
                unresolvedMethods.put(binding, list);
            }
            list.add(method);
        } else {
            node.addIntertypeMethod(method);
        }
    }

    private void resolveMethods(TypeDeclaration type) {
        Iterator<TypeBinding> i = unresolvedMethods.keySet().iterator();
        while (i.hasNext()) {
            TypeBinding b = i.next();
            TypeNode node = getTypeNode(b);
            if (node != null) {
                List<AbstractMethodDeclaration> list = unresolvedMethods.get(b);
                if (list != null) {
                    Iterator<AbstractMethodDeclaration> m = list.iterator();
                    while (m.hasNext()) {
                        node.addIntertypeMethod(m.next());
                        m.remove();
                    }
                }
            }
        }
    }

    public void addRelationship(TypeNode from, TypeNode to, RelationshipEdgeKind k) {
        RelationshipEdge e = new RelationshipEdge(k);
        e.setSourceNode(from);
        e.setDestinationNode(to);

        this.add(e);
    }

    public Set<TypeDeclaration> getInheritanceHierarchy(SourceTypeBinding binding) {
        Set<TypeDeclaration> h = new HashSet<TypeDeclaration>();
        if (binding != null) {
            TypeNode node = getTypeNode(binding);
            if (node != null) {
                Queue<TypeNode> queue = new LinkedList<TypeNode>();

                // Get superclasses.
                queue.add(node);
                while (!queue.isEmpty()) {
                    TypeNode n = queue.poll();

                    // Add to result.
                    h.add(n.getTypeDeclaration());
                    Iterator<GraphNode> i = n.getDestinationNodeSet().iterator();
                    while (i.hasNext()) {
                        queue.add((TypeNode) i.next());
                    }
                }

                // Get subclasses.
                queue.add(node);
                while (!queue.isEmpty()) {
                    TypeNode n = queue.poll();

                    // Add to result.
                    h.add(n.getTypeDeclaration());
                    Iterator<GraphNode> i = n.getSourceNodeSet().iterator();
                    while (i.hasNext()) {
                        queue.add((TypeNode) i.next());
                    }
                }
            }
        }

        return h;
    }

    public static AbstractMethodDeclaration getStaticCallee(ASTNode node) {
        MethodBinding mb = null;

        if (node instanceof MessageSend) {
            MessageSend ms = (MessageSend) node;
            mb = ms.binding;
        } else if (node instanceof AllocationExpression) {
            AllocationExpression ae = (AllocationExpression) node;
            mb = ae.binding;
        } else if (node instanceof ExplicitConstructorCall) {
            ExplicitConstructorCall ecc = (ExplicitConstructorCall) node;
            mb = ecc.binding;
        }

        return (mb == null ? null : mb.sourceMethod());
    }

    public Set<AbstractMethodDeclaration> getCallees(SourceTypeBinding binding, AbstractMethodDeclaration sourceMethod) {
        if (null == binding) {
            throw new NullPointerException("Binding cannot be null!");
        }

        return getOverridingMethods(binding, sourceMethod);
    }

    public Set<AbstractMethodDeclaration> getOverridingMethods(SourceTypeBinding binding,
            AbstractMethodDeclaration sourceMethod) {
        Set<AbstractMethodDeclaration> result = new HashSet<AbstractMethodDeclaration>();

        if (null != sourceMethod) {
            Iterator<TypeDeclaration> typeIter = getInheritanceHierarchy(binding).iterator();
            while (typeIter.hasNext()) {
                TypeDeclaration td = typeIter.next();
                Iterator<AbstractMethodDeclaration> i = getTypeNode(td.binding).getMethodDeclarations().iterator();
                while (i.hasNext()) {
                    AbstractMethodDeclaration m = i.next();
                    if (!m.isAbstract() && compareMethod(m, sourceMethod)) {
                        result.add(m);
                    }
                }
            }
        }

        return result;
    }

    public static boolean compareMethod(AbstractMethodDeclaration m1, AbstractMethodDeclaration m2) {
        if (m1 == m2) {
            return true;
        }

        if (null == m1 || null == m2) {
            return false;
        }

        MethodBinding b1 = m1.binding;
        MethodBinding b2 = m2.binding;
        if ((null == b1 && null != b2) || (null != b1 && null == b2)) {
            return false;
        }

        if (null != b1 && null != b2) {
            // Visibility.
            if (b1.isPrivate() || b2.isPrivate()) {
                return false;
            }

            // Modifiers.
            if (b1.isStatic() && b2.isStatic()) {
                return false;
            }
        }

        // Compare names.
        String name1 = getShortName(m1);
        String name2 = getShortName(m2);
        if (!name1.equals(name2)) {
            return false;
        }

        // // Compare arguments.
        // Argument[] a1 = m1.arguments;
        // Argument[] a2 = m2.arguments;
        // if (null == a1 || null == a2) {
        // return (null == a1 && null == a2) ? true : false;
        // }
        // if (a1.length != a2.length) {
        // return false;
        // }
        // int length = a1.length;
        // for (int i = 0; i < length; ++i) {
        // if (a1[i].type != a2[i].type) {
        // return false;
        // }
        // }

        return true;
    }

    private static String getShortName(AbstractMethodDeclaration m) {
        try {
            StringBuffer s = new StringBuffer();

            if (m instanceof InterTypeMethodDeclaration) {
                s.append(((InterTypeMethodDeclaration) m).getDeclaredSelector());
            } else {
                s.append(m.selector);
            }
            s.append('(');
            Argument[] args = m.arguments;
            int i = 0;
            if (m instanceof InterTypeMethodDeclaration) {
                i++;
            }
            for (; args != null && i < args.length; ++i) {
                s.append(args[i].binding.type.readableName());
                if (i != args.length - 1) {
                    s.append(',');
                }
            }
            s.append(')');

            return s.toString();
        } catch (Throwable e) {
            return "(Unknown method name)";
        }
    }
    
    public static String getFullName(AbstractMethodDeclaration m) {
        try {
            StringBuffer b = new StringBuffer();
            String s = m.binding.toString();
            String rType = null;
            String mName = null;
            
            if (s != null && s.indexOf("NULL TYPE") == 0) {
                rType = "NULL TYPE";
            } else {
                rType = s.substring(0, s.indexOf(' '));
            }
            mName = s.substring(rType.length() + 1);
            
            b.append(rType);
            b.append(' ');
            b.append(m.binding.declaringClass.fPackage.shortReadableName());
            b.append('.');
            b.append(m.binding.declaringClass.sourceName());
            b.append(".");
            b.append(mName);
            
            String result = b.toString().trim();
            
            return result;
        } catch (Throwable t) {
            return "(Unknown method name)";
        }
    }

    public TypeDeclaration find(TypeBinding binding) {
        TypeNode node = getTypeNode(binding);

        return (node == null ? null : node.getTypeDeclaration());
    }

    private TypeNode getTypeNode(TypeBinding binding) {
        return typeNodeMapping.get(binding);
    }
}
